All files / src/app/api/proxy/subtitle route.ts

0% Statements 0/50
0% Branches 0/30
0% Functions 0/3
0% Lines 0/47

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122                                                                                                                                                                                                                                                   
import { NextRequest, NextResponse } from 'next/server';
import { hasAuth, requireAuthEnabled, validateUpstreamUrl } from '../proxyUtils';
 
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
 
/**
 * Subtitle proxy and converter API route.
 * 
 * IMPORTANT: This API route requires a Node.js server runtime to proxy and convert subtitle files.
 * The 'force-dynamic' export above ensures this route runs at runtime.
 */
 
// Convert SRT format to WebVTT format
function srtToWebVtt(srtContent: string): string {
  // Add WebVTT header
  let webvtt = 'WEBVTT\n\n';
 
  // Split into subtitle blocks
  const blocks = srtContent.trim().split('\n\n');
 
  for (const block of blocks) {
    const lines = block.trim().split('\n');
    if (lines.length < 3) continue;
 
    // Skip the sequence number (first line)
    const timeLine = lines[1];
    const textLines = lines.slice(2);
 
    // Convert SRT timestamp format to WebVTT format
    // SRT: 00:01:24,667 --> 00:01:26,043
    // WebVTT: 00:01:24.667 --> 00:01:26.043
    const webvttTimeLine = timeLine.replace(/,/g, '.');
 
    // Add cue to WebVTT
    webvtt += `${webvttTimeLine}\n`;
    webvtt += `${textLines.join('\n')}\n\n`;
  }
 
  return webvtt;
}
 
export async function GET(request: NextRequest) {
  try {
    const url = request.nextUrl.searchParams.get('url');
 
    if (!url) {
      return NextResponse.json({ error: 'URL parameter is required' }, { status: 400 });
    }
 
    if (requireAuthEnabled() && !hasAuth(request)) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
    }
 
    // Allow same-origin /api/* paths (used for internal subtitle endpoints behind rewrites),
    // but block arbitrary relative URLs. External URLs must pass allowlist validation.
    let upstreamUrl: URL;
    if (url.startsWith('/')) {
      if (!url.startsWith('/api/')) {
        return NextResponse.json({ error: 'Only /api/* relative paths are allowed' }, { status: 400 });
      }
      upstreamUrl = new URL(`${request.nextUrl.protocol}//${request.nextUrl.host}${url}`);
    } else {
      const validated = validateUpstreamUrl(url);
      if (!validated.ok) {
        return NextResponse.json({ error: validated.error }, { status: validated.status });
      }
      upstreamUrl = validated.url;
    }
 
    // Fetch the original subtitle file
    const response = await fetch(upstreamUrl, {
      headers: {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
        // Forward authorization if needed
        ...(request.headers.get('cookie') ? { 'Cookie': request.headers.get('cookie')! } : {})}});
 
    if (!response.ok) {
      console.error('🎬 Failed to fetch subtitle:', response.status, response.statusText);
      return NextResponse.json({ error: 'Failed to fetch subtitle' }, { status: response.status });
    }
 
    const contentType = response.headers.get('content-type') || '';
    const content = await response.text();
 
 
    let finalContent = content;
    const finalContentType = 'text/vtt; charset=utf-8';
 
    // Convert SRT to WebVTT if needed
    if (contentType.includes('subrip') || contentType.includes('srt') ||
      content.includes('-->') && !content.startsWith('WEBVTT')) {
      finalContent = srtToWebVtt(content);
    } else if (contentType.includes('vtt') || content.startsWith('WEBVTT')) {
    } else {
      finalContent = srtToWebVtt(content);
    }
 
    // Return with proper headers for subtitle consumption
    return new NextResponse(finalContent, {
      headers: {
        'Content-Type': finalContentType,
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Methods': 'GET, OPTIONS',
        'Access-Control-Allow-Headers': 'Content-Type',
        'Cache-Control': 'public, max-age=3600'}});
 
  } catch (error) {
    console.error('🎬 Subtitle proxy error:', error);
    return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
  }
}
 
export async function OPTIONS() {
  return new NextResponse(null, {
    status: 200,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type'}});
}